#include "ServerSyncAgent.h"
#include "macros.h"
#include "BankDatabase.h"
#include "BankServer.h"

using namespace std;

// the agent responsible for maintaining primary and backup databases in sync
ServerSyncAgent g_syncAgent;

////////////////////////////////
//                            //
// local function definitions //
//                            //
////////////////////////////////

#define RpcCalling(ARGS,FUNC) RpcCallingImpl (ARGS,FUNC,#FUNC)

#define RpcRetCalling(ARGS,FUNC,RET) RpcCallingImpl (ARGS,FUNC,#FUNC,RET)


//////////////////////////////
//                          //
// RPC function definitions //
//                          //
//////////////////////////////

ActionRet_t *
synctwobalance_1_svc (SyncTwoBalanceArgs_t *argp, struct svc_req *rqstp)
{
  DEBUG_LOG ("synctwobalance_1_svc () called");

	static ActionRet_t  result;

  try
    {
    int updateCount = argp->expectedUpdateCount;
    string cusId1 (argp->cusId1.cusId1_val, argp->cusId1.cusId1_len);
    double balance1 = argp->balance1;
    string cusId2 (argp->cusId2.cusId2_val, argp->cusId2.cusId2_len);
    double balance2 = argp->balance2;    

    g_bankDb.BeginTransaction ();
    CHECK_UPDATE_COUNT (argp);

    g_bankDb.SetBalance (cusId1, balance1);
    if (!cusId2.empty ())
      {
      g_bankDb.SetBalance (cusId2, balance2);
      }

    g_bankDb.IncrementUpdateCount (CLIENT_ID);
    g_bankDb.CommitTransaction ();
    result.status = 0; // OK
    WRITE_RESULT_STRING (result, "Sync completed for UpdateCount = " << updateCount);
    }
  catch (runtime_error e)
    {
    g_bankDb.RollbackTransaction ();
    result.status = 1; // error
    WRITE_RESULT_STRING (result, e.what ());
    }

  result.serverUpdateCount = g_bankDb.GetUpdateCount (CLIENT_ID);
	return &result;
}

ActionRet_t *
syncme_1_svc (void *argp, struct svc_req *rqstp)
{
  DEBUG_LOG ("syncme_1_svc () called");

	static ActionRet_t  result;

  try
    {
	  g_syncAgent.Open (g_bankDb.GetDatabaseName ());

    result.status = 0; // OK
    WRITE_RESULT_STRING (result, "prepared to call ServerSyncAgent::Sync ()");
    //DEBUG_LOG (result.statusMsg.statusMsg_val);
    }
  catch (runtime_error e)
    {
    result.status = 1; //error
    WRITE_RESULT_STRING (result, e.what ());
    DEBUG_WRN ("SyncToMe () aborted - " << e.what ());
    }

  result.serverUpdateCount = g_bankDb.GetUpdateCount (CLIENT_ID);
	return &result;
}

///////////////////////////
//                       //
// C++ class definitions //
//                       //
///////////////////////////

/// return false if RPC call is failed; true otherwise
template <typename RET, typename ARGS>
bool 
ServerSyncAgent::RpcCallingImpl (ARGS* args, RET* (*RpcFunc)(ARGS*,CLIENT*), 
                                 string RpcFuncName, int* serverUpdateCount)
  {
  const int maxRetry = 2;
  for (int retry=0; retry<maxRetry; retry++)
    {
	  RET* result = RpcFunc (args, m_clnt);
	  if (result == (ActionRet_t *) NULL) 
      {
		  DEBUG_WRN (clnt_sperror (m_clnt, "call failed") << " - " << RpcFuncName.substr (1));

      // retry calling this RPC function
      if (RecreateClient ())
        {
        continue;
        }
      else
        {
        DEBUG_WRN ("Failed recreating a client handle for the remote server (" << m_backupSvc << ")");
        return false;
        }
	    }
    else 
      {
      if (!result->status)
        {
        cout << "OK - " << result->statusMsg.statusMsg_val << endl << endl;
        }
      else
        {
        cout << "WRN - " << result->statusMsg.statusMsg_val << endl << endl;
        }
      if (serverUpdateCount != NULL)
        {
        *serverUpdateCount = result->serverUpdateCount;
        }

      //DEBUG_LOG ("UpdateCount at server = " << result->serverUpdateCount);
      return true;
      }  
    }
  DEBUG_WRN ("Retried calling " << RpcFuncName.substr (1) << " by " << maxRetry << " times but still failed!");
  return false;
  }

ServerSyncAgent::ServerSyncAgent (void) 
: m_clnt (NULL), m_db (0), m_backupSvc ("")
, m_record1CusStmt (0), m_record2CusStmt (0), m_selectStmt (0), m_setSyncFinishedTrueStmt (0)
, m_dbIsOpened (false)
  {}

ServerSyncAgent::ServerSyncAgent (string bankDbName, string backupSvc) 
: m_clnt (NULL), m_db (0), m_backupSvc ("")
, m_record1CusStmt (0), m_record2CusStmt (0), m_selectStmt (0), m_setSyncFinishedTrueStmt (0)
, m_dbIsOpened (false)
  {
  Open (bankDbName, backupSvc);
  }

ServerSyncAgent::~ServerSyncAgent (void)
  {
  Close ();
  }

void 
ServerSyncAgent::Open (string bankDbName, string backupSvc)
  {
  if (!backupSvc.empty ())
    {
    //DEBUG_LOG ("Setting the backup server hostname (" << backupSvc << ") to ServerSyncAgent");
    m_backupSvc = backupSvc;
    }

  if (!m_dbIsOpened)
    {
    /****************************/
    /* Open database connection */
    /****************************/

    // open a connection to the database file
    int status = sqlite3_open (bankDbName.c_str (), &m_db);
    SQLITE_STATUS_CHECK (status, "Unable to open the database \"" << bankDbName << "\" - ");
    
    // create the table if it does not exist yet
    char* errmsg;
    status = sqlite3_exec (m_db,
      "create table if not exists SyncAgentTable (\
      UpdateCount integer primary key on conflict replace,\
      CustomerId1 text, \
      Balance1 real, \
      CustomerId2 text, \
      Balance2 real, \
      SyncFinished integer default 0);",
      NULL, 
      NULL,
      &errmsg
      );
    SQLITE_STATUS_CHECK (status, errmsg << " - ");
    sqlite3_free (errmsg);

    // create sqlite3 prepared statement for recording one new balances
    status = sqlite3_prepare_v2 (m_db,
      "insert or replace into SyncAgentTable (\
      UpdateCount, \
      CustomerId1, \
      Balance1) values (\
      @updateCount, \
      @cusId1,\
      @balance1);",
      -1,
      &m_record1CusStmt,
      NULL);
    SQLITE_STATUS_CHECK (status, "Unable to prepare the \"m_record1CusStmt\" INSERT statement for SyncAgentTable - ");

    // create sqlite3 prepared statement for recording two new balances
    status = sqlite3_prepare_v2 (m_db,
      "insert or replace into SyncAgentTable (\
      UpdateCount, \
      CustomerId1, \
      Balance1, \
      CustomerId2, \
      Balance2) values (\
      @updateCount, \
      @cusId1,\
      @balance1, \
      @cusId2, \
      @balance2);",
      -1,
      &m_record2CusStmt,
      NULL);
    SQLITE_STATUS_CHECK (status, "Unable to prepare the \"m_record2CusStmt\" INSERT statement for SyncAgentTable - ");

    // create sqlite3 prepared statement for selecting pending BankTable updates instruction
    status = sqlite3_prepare_v2 (m_db,
      "select * from SyncAgentTable where SyncFinished = 0;",
      -1,
      &m_selectStmt,
      NULL);
    SQLITE_STATUS_CHECK (status, "Unable to prepare the \"m_selectStmt\" SELECT statement for SyncAgentTable - ");

    // create sqlite3 prepared statement for updating the SyncFinished bit of a row
    status = sqlite3_prepare_v2 (m_db,
      "update SyncAgentTable set SyncFinished = 1 where UpdateCount = @updateCount;",
      -1,
      &m_setSyncFinishedTrueStmt,
      NULL);
    SQLITE_STATUS_CHECK (status, "Unable to prepare the \"m_setSyncFinishedTrueStmt\" UPDATE statement for SyncAgentTable - ");

    m_dbIsOpened = true;
    }

  if (m_clnt == NULL)
    {
    /*********************/
    /* create RPC handle */
    /*********************/
    
	  CreateClient ();
    }
  }

void 
ServerSyncAgent::Close ()
  {
  if (m_dbIsOpened)
    {
    int status = sqlite3_finalize (m_record1CusStmt);
    SQLITE_STATUS_CHECK (status, "Unable to finalize \"m_record1CusStmt\" - ");
    status = sqlite3_finalize (m_record2CusStmt);
    SQLITE_STATUS_CHECK (status, "Unable to finalize \"m_record2CusStmt\" - ");
    status = sqlite3_finalize (m_selectStmt);
    SQLITE_STATUS_CHECK (status, "Unable to finalize \"m_selectStmt\" - ");
    status = sqlite3_finalize (m_setSyncFinishedTrueStmt);
    SQLITE_STATUS_CHECK (status, "Unable to finalize \"m_setSyncFinishedTrueStmt\" - ");    
    status = sqlite3_close (m_db);
    SQLITE_STATUS_CHECK (status, "Unable to close the database connection - ");

    m_dbIsOpened = false;
    }

  if (m_clnt != NULL)
    {
    clnt_destroy (m_clnt);
    }
  }

bool 
ServerSyncAgent::CreateClient ()
  {
  m_clnt = clnt_create (m_backupSvc.c_str (), SyncAgent, SyncAgentVer, "udp");
  if (m_clnt == NULL) 
   {
	  DEBUG_WRN ("ServerSyncAgent is unable to create a client handle for a remote backup server - " 
      << clnt_spcreateerror (m_backupSvc.c_str ()));
    return false;
    }
  else
    {
    DEBUG_LOG ("client handle created successfully for the RPC server - " << m_backupSvc);
    return true;
    }
  }

bool 
ServerSyncAgent::RecreateClient ()
  {
  if (m_clnt != NULL)
    {
    clnt_destroy (m_clnt);
    }
  return CreateClient ();
  }

void 
ServerSyncAgent::Record (string cusId1, double balance1, int updateCount, 
                         string cusId2, double balance2)
  {
  sqlite3_stmt* recordStmt;
  if (cusId2.empty ())
    {
    recordStmt = m_record1CusStmt;
    }
  else
    {
    recordStmt = m_record2CusStmt;
    }

  int status = sqlite3_bind_text (recordStmt, 
    sqlite3_bind_parameter_index (recordStmt, "@cusId1"),
    cusId1.c_str (),
    cusId1.size (),
    SQLITE_STATIC
    );
  SQLITE_STATUS_CHECK (status, "Unable to bind cusId1 (" << cusId1 << ") into recordStmt - ");

  status = sqlite3_bind_double (recordStmt, 
    sqlite3_bind_parameter_index (recordStmt, "@balance1"),
    balance1
    );
  SQLITE_STATUS_CHECK (status, "Unable to bind balance1 (" << balance1 << ") into recordStmt - ");

  status = sqlite3_bind_int (recordStmt, 
    sqlite3_bind_parameter_index (recordStmt, "@updateCount"),
    updateCount
    );
  SQLITE_STATUS_CHECK (status, "Unable to bind updateCount (" << updateCount << ") into recordStmt - ");

  if (!cusId2.empty ())
    {
    status = sqlite3_bind_text (recordStmt, 
    sqlite3_bind_parameter_index (recordStmt, "@cusId2"),
    cusId2.c_str (),
    cusId2.size (),
    SQLITE_STATIC
    );
    SQLITE_STATUS_CHECK (status, "Unable to bind cusId2 (" << cusId2 << ") into recordStmt - ");

    status = sqlite3_bind_double (recordStmt, 
      sqlite3_bind_parameter_index (recordStmt, "@balance2"),
      balance2
      );
    SQLITE_STATUS_CHECK (status, "Unable to bind balance2 (" << balance2 << ") into recordStmt - ");
    }

  // execute the SQL statement
  status = sqlite3_step (recordStmt);
  SQLITE_STATUS_CHECK (status, "Unable to execute \"recordStmt\" - ");
  status = sqlite3_reset (recordStmt);
  SQLITE_STATUS_CHECK (status, "Unable to reset \"recordStmt\" - ");
  }

void 
ServerSyncAgent::SetSyncFinished (int updateCount)
  {
  int status = sqlite3_bind_int (m_setSyncFinishedTrueStmt, 
    sqlite3_bind_parameter_index (m_setSyncFinishedTrueStmt, "@updateCount"),
    updateCount
    );
  SQLITE_STATUS_CHECK (status, "Unable to bind updateCount (" << updateCount << ") into m_setSyncFinishedTrueStmt - "); 

  // execute the SQL statement
  status = sqlite3_step (m_setSyncFinishedTrueStmt);
  SQLITE_STATUS_CHECK (status, "Unable to execute \"m_setSyncFinishedTrueStmt\" - ");
  // reset the SQL statement
  status = sqlite3_reset (m_setSyncFinishedTrueStmt);
  SQLITE_STATUS_CHECK (status, "Unable to reset \"m_setSyncFinishedTrueStmt\" - ");
  }

void 
ServerSyncAgent::Sync ()
  {
  DEBUG_LOG ("ServerSyncAgent::Sync () called");

  if (m_dbIsOpened && (m_clnt != NULL))
    {
    while (true)
      {
      // execute the SQL statement
      int status = sqlite3_step (m_selectStmt);
      SQLITE_STATUS_CHECK (status, "Unable to execute \"m_selectStmt\" - ");

      // check if there are rows ready for reading
      if (status != SQLITE_ROW)
        {
        break;
        }

      // read the column values of each row
      int updateCount    = sqlite3_column_int (m_selectStmt, 0);
      const char* cusId1 = (const char*) sqlite3_column_text (m_selectStmt, 1);
      double balance1    = sqlite3_column_double (m_selectStmt, 2);
      const char* cusId2 = (const char*) sqlite3_column_text (m_selectStmt, 3);
      double balance2    = sqlite3_column_double (m_selectStmt, 4);

      // initialize RPC calling argument
      SyncTwoBalanceArgs_t args;
      args.expectedUpdateCount = updateCount;
      args.cusId1.cusId1_val = const_cast<char*> (cusId1);
      args.cusId1.cusId1_len = strlen (cusId1);
      args.balance1 = balance1;
      if (cusId2 != NULL)
        {
        args.cusId2.cusId2_val = const_cast<char*> (cusId2);
        args.cusId2.cusId2_len = strlen (cusId2);
        args.balance2 = balance2;
        }
      else
        {
        args.cusId2.cusId2_val = NULL;
        args.cusId2.cusId2_len = 0;
        args.balance2 = 0;
        }

      // call server function via RPC
      int serverUpdateCount = 0;
      bool callIsGood = RpcRetCalling (&args, &synctwobalance_1, &serverUpdateCount);

      DEBUG_LOG ("RpcRetCalling (&args, &synctwobalance_1, &serverUpdateCount) = " << callIsGood);

      if (callIsGood)
        {// OK
        if (serverUpdateCount >= updateCount)
          {
          SetSyncFinished (updateCount);
          DEBUG_LOG ("ServerSyncAgent::Sync () to backup server sccessfully for the UpdateCount (" 
            << updateCount << ")");
          }
        else
          {
          DEBUG_ERR ("ServerSyncAgent::Sync () aborted due to missing update actions from UpdateCount = " 
            << serverUpdateCount+1 << " to " 
            << updateCount << " at the remote backup server.");
          break;
          }
        }
      else
        {// RPC error
        DEBUG_ERR ("ServerSyncAgent::Sync () aborted due to RPC call failed at UpdateCount = " 
          << updateCount);
        break;
        }
      }

    int status = sqlite3_reset (m_selectStmt);
    SQLITE_STATUS_CHECK (status, "Unable to reset \"m_selectStmt\" - ");
    }
  else
    {
    if (!m_dbIsOpened)
      {
      DEBUG_ERR ("ServerSyncAgent::Sync () aborted due to missing database connection!");
      }
    if (m_clnt == NULL)
      {
      DEBUG_WRN ("ServerSyncAgent::Sync () aborted due to missing RPC client handle!");
      }
    }
  }

void 
ServerSyncAgent::SyncToMe ()
  {
  DEBUG_LOG ("ServerSyncAgent::SyncToMe () called");

  if (m_clnt != NULL)
    {
    // call server function via RPC
    const char* dummy = "dummy";
    RpcCalling ((void*) dummy, &syncme_1);
    }
  else
    {
    DEBUG_WRN ("ServerSyncAgent::SyncToMe () aborted due to missing RPC client handle!");
    }
  }

///////////////////////////
//                       //
// Test case definitions //
//                       //
///////////////////////////

TEST (ServerSyncAgentMethodTest, RecordAndSyncTest)
  {
  g_syncAgent.Record ("wilson", 111.11, 1);
  g_syncAgent.Record ("thong",  333.33, 2, "to",    444.44);
  g_syncAgent.Record ("wang",   555.55, 3, "tsz",   666.66);
  g_syncAgent.Record ("kit",    777.77, 4, "ying",  888.77);

  g_syncAgent.Sync ();
  g_syncAgent.SyncToMe ();
  }

TEST (ServerSyncAgentMethodTest, RecreateClientTest)
  {
  g_syncAgent.RecreateClient ();
  }
